package prac7;
import java.io.BufferedReader;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.ArrayList;
import java.util.Locale;
import dictionary.Dictionary;

/**
 * Part of a prac using recursive to implement a simple predictive text system.
 * 
 * @author			Menghong Li
 * @author			Brendan Peterson
 * @modified		Menghong Li & Brendan Peterson(September 2011)
 * @since			12th September 2011
 * @input			{@link #main(String[])}: command to execute
 * @input			{@link #readDigits()}: reads from screen
 * @output			{@link #main(String[])}: command response
 * @errorHandling	FileNotFoundException: when the file does not exist.
 */
public class Prac7 {

	/**
	 * Prompts user for a string, calls {@link #isAllDigits(String)} to check if string only
	 * contains numeric values, return the string if is true.
	 * 
	 * @post		String is returned if all characters are numeric values, otherwise error message is printed.
	 * @complexity	Best and worst case complexity: O(R + I) where R is the complexity of reading a string from System.in 
	 * 														 where I is the complexity of {@link #isAllDigits(String)}.
	 */
	public static String readDigits() throws IOException{
		InputStreamReader input = new InputStreamReader(System.in);
		BufferedReader keyboard = new BufferedReader(input);
		String str = keyboard.readLine();

		if(isAllDigits(str)) {
			return str;
		} else {
			System.out.println("The input is not valid");
			return null;
		}
	}

	/**
	 * Recursive method to checks if a string contains only numeric values. 
	 * 
	 * @param	str	the string to be checked
	 * @post	the	string is not altered
	 * @complexity	Best and worst case:   O(P*N)
	 * 				where 	P is the complexity of {@link java.lang.String#charAt(int)}
	 * 						N is the length of the string
	 * @return	true if string only contains numeric values, false otherwise
	 */
	public static boolean isAllDigits(String str) {
		if(str.length()==0) {
			return true;
		}
		else if(str.charAt(0) < '0' && str.charAt(0) > '9') {
			return false;
		}
		else {
			isAllDigits(str.substring(1));
		}
		return true;
	}

	/**
	 * Prints all the combinations of letters that a string of numeric values could represent.
	 * 
	 * @param 		str  the string represent the numeric values.
	 * @post  		return an ArryaList which contains all combinations.
	 * @complexity	best case and worst case: O(M)
	 * 				where	M is the complexity of {@link #printStringsAux(String)}
	 */
	public static ArrayList<String> printStrings(String str) {
		ArrayList<Character> list = new ArrayList<Character>();
		ArrayList<String> rawList = new ArrayList<String>();
		printStringsAux(list, rawList, str);
		return rawList;
	}

	/**
	 * Recursively obtain all the combinations of letters that a string of numeric values could represent.
	 * 
	 * @param 		rawlist the combinations of characters so far
	 * @param		str		the remaining string
	 * @param 		list  	the current combination of characters 
	 * @post  		All the combinations are stored in the rawlist
	 * @complexity	best case of O(C*N) - the digits in the string represent empty combinations (i.e  "1111" or "0000") 
	 * 				worst case of O(N1^N*C) 
	 * 				where	N is the number of digits in the string
	 * 						N1 is the length of the list, which returned by getCorrespondsLetters(Character chr)
	 * 						C is the complexity of getCorrespondsLetters(Character chr).
	 */
	public static void printStringsAux(ArrayList<Character> list, ArrayList<String> rawlist,String str){
		if(str.isEmpty()) {
			String word = "";
			for (Character chr : list) {
				word+= chr;
			}
			rawlist.add(word);
		} else {
			ArrayList<Character> correspondsLetters = new ArrayList<Character>();
			correspondsLetters = getCorrespondsLetters(str.charAt(0));
			if(correspondsLetters.isEmpty()) {
				printStringsAux(list, rawlist, str.substring(1));
			}
			for (Character chr : correspondsLetters) {
				list.add(chr);
				printStringsAux(list, rawlist, str.substring(1));
				list.remove(list.size()-1);
			}
		}
	}

	/**
	 * Return a list which contains all the corresponds letters of the string.
	 * 
	 * @param 		str the string which the user prompts.
	 * @post  		Return a list which contains all the corresponds letters of the string
	 * @complexity	best case O(3*I) - has 3 corresponds letters
	 * 			   	worst case O(4*I) - has 4 corresponds letters
	 * 				where	 I is the complexity of {@link ArrayList#add(Character)}.
	 */
	public static ArrayList<Character> getCorrespondsLetters(Character chr) {
		ArrayList<Character> correspondsLetters = new ArrayList<Character>();
		if(chr.compareTo('2') == 0) {
			correspondsLetters.add('A');
			correspondsLetters.add('B');
			correspondsLetters.add('C');
		} 
		else if(chr.compareTo('3') == 0)
		{
			correspondsLetters.add('D');
			correspondsLetters.add('E');
			correspondsLetters.add('F');
		}
		else if(chr.compareTo('4') == 0) {
			correspondsLetters.add('G');
			correspondsLetters.add('H');
			correspondsLetters.add('I');
		}
		else if(chr.compareTo('5') == 0) {
			correspondsLetters.add('J');
			correspondsLetters.add('K');
			correspondsLetters.add('L');
		}
		else if(chr.compareTo('6') == 0) {
			correspondsLetters.add('M');
			correspondsLetters.add('N');
			correspondsLetters.add('O');
		}
		else if(chr.compareTo('7') == 0) {
			correspondsLetters.add('P');
			correspondsLetters.add('Q');
			correspondsLetters.add('R');
			correspondsLetters.add('S');
		}
		else if(chr.compareTo('8') == 0) {
			correspondsLetters.add('T');
			correspondsLetters.add('U');
			correspondsLetters.add('V');
		}
		else if(chr.compareTo('9') == 0) {
			correspondsLetters.add('W');
			correspondsLetters.add('X');
			correspondsLetters.add('Y');
			correspondsLetters.add('Z');
		}
		return correspondsLetters;
	}

	/**
	 * Prints all the words that a string of numeric values could represent.
	 * 
	 * @param 		str the string used to generate words
	 * @complexity	Best and worst case: O(N*F)
	 * 				where	N is the number of words in the rawlist,
	 * 						F is the complexity of {@link dictionary.Dictionary#findWord(String)}
	 */
	public static void printWords(String str) throws FileNotFoundException {
		ArrayList<String> rawlist = printStrings(str);
		Dictionary dict = new Dictionary();
		dict.readFromFile("words.txt");
		for (String word : rawlist) {
			if(word.compareTo("") != 0){
				if(dict.findWord(word)) {
					System.out.println(word.toLowerCase());
				}
			}
		}
	}

	/**
	 * Prints all the words beginning with a prefix that a string of numeric values could represent.
	 * 
	 * @param str the string used to generate words
	 * @post  all words with prefix are printed
	 * @complexity	best case and worst case: O(W+(N*(P+N1*Q)))
	 * 				where	W is the complexity of {@link printStrings(str)}}
	 * 						N is the number of words in the rawlist
	 * 						P is the complexity of {@link dictionary.Dictionary#getWordsWithPrefix(String)}
	 * 						N1 is the number of words beginning with a prefix
	 * 						Q is the complexity of {@link java.io.PrintStream#println(String)}
	 * 						
	 */
	public static void  printPrefixWords(String str) throws FileNotFoundException{
		Dictionary dict = new Dictionary();
		dict.readFromFile("words.txt");
		ArrayList<String> words = new ArrayList<String>();
		ArrayList<String> rawlist = printStrings(str);
		for (String word : rawlist) {
			if(word.compareTo("") !=0)
				words = dict.getWordsWithPrefix(word.toLowerCase(Locale.ENGLISH));
			for (String w : words) {
				System.out.println(w);
			}
		}
	}

	/**
	 * Prints only the words where the resultant string is a prefix of at least one word in the dictionary.
	 * 
	 * @param str the string used to generate words
	 * @post  all words with prefix are printed
	 * @complexity	best case and worst case: O(W+(N*(P+N1*Q)))
	 * 				where	W is the complexity of {@link printOptStrings(str)}}
	 * 						N is the number of words in the rawlist
	 * 						P is the complexity of {@link dictionary.Dictionary#getWordsWithPrefix(String)}
	 * 						N1 is the number of words beginning with a prefix
	 * 						Q is the complexity of {@link java.io.PrintStream#println(String)}
	 * 						
	 */
	public static void  printOptPrefixWords(String str) throws FileNotFoundException {
		Dictionary dict = new Dictionary();
		dict.readFromFile("words.txt");
		ArrayList<String> words = new ArrayList<String>();
		ArrayList<String> rawlist = printOptStrings(str, dict);
		for (String word : rawlist) {
			if(word.compareTo("") !=0){
				words = dict.getWordsWithPrefix(word.toLowerCase(Locale.ENGLISH));
				for (String w : words) {
					System.out.println(w);
				}
			}
		}
	}

	/**
	 * Prints all the combinations of letters that a string of numeric values could represent considering the
	 * combination is the prefix of a word.
	 * 
	 * @param 		str  the string represent the numeric values.
	 * @post  		return an ArryaList which contains all combinations.
	 * @complexity	best case and worst case: O(M)
	 * 				where	M is the complexity of {@link #printOptStringsAux(String)}
	 */
	public static ArrayList<String> printOptStrings(String str, Dictionary dict){
		ArrayList<Character> list = new ArrayList<Character>();
		ArrayList<String> rawList = new ArrayList<String>();
		printOptStringsAux(list, rawList, str, dict);
		return rawList;
	}

	/**
	 * Recursively obtains all the combinations of letters that a string of numeric values could represent considering
	 * the combination is the prefix of a word.
	 * 
	 * @param 		rawlist the combinations of characters so far
	 * @param		str		the remaining string
	 * @param 		list  	the current combination of characters 
	 * @post  		All the combinations are stored in the rawlist
	 * @complexity	best case of O(M + C*N) - the digits in the string represent empty combinations (i.e  "1111" or "0000") 
	 * 				worst case of O(M + N1^N*C) 
	 * 				where	M is the complexity of {@link dictionary.Dictionary#hasPrefix(String)}
	 * 						N is the number of digits in the string
	 * 						N1 is the length of the list, which returned by getCorrespondsLetters(Character chr)
	 * 						C is the complexity of getCorrespondsLetters(Character chr).
	 */
	public static void printOptStringsAux(ArrayList<Character> list, ArrayList<String> rawlist,String str ,Dictionary dict) {
		String word = "";
		for (Character chr : list) { word+= chr; }
		if(!dict.hasPrefix(word)) {
			return;
		}
		if(str.isEmpty()) {
			rawlist.add(word);
		}
		else {
			ArrayList<Character> correspondsLetters = new ArrayList<Character>();
			correspondsLetters = getCorrespondsLetters(str.charAt(0));
			if(correspondsLetters.isEmpty()) {
				printOptStringsAux(list, rawlist, str.substring(1), dict);
			}
			for (Character chr : correspondsLetters) {
				list.add(chr);
				printOptStringsAux(list, rawlist, str.substring(1), dict);
				list.remove(list.size()-1);
			}
		}
	}

	
	/**
	 * 
	 * A simple predictive text system based on T9.
	 * 
	 * @param args
	 * @Complexity	Best and Worst case: O(C + D + G + M + N )
	 * 				Where C is the complexity of readDigits()
	 * 					  D is the complexity of dict.readFromFile(String)
	 * 					  G is the complexity of printWords(String)
	 * 					  M is the complexity of printPrefixWords(String)
	 * 					  N is the complexity of printOptPrefixWords(String)
	 * 					
	 * @throws FileNotFoundException
	 */
	public static void main(String[] args) throws FileNotFoundException {
		System.out.println("Please enter a combination of numbers.");
		String str ="";
//		double timeTaken;

		try {
			str = readDigits();
		} catch (IOException e) {
			System.out.println("Error reading from console.");
		}

		printStrings(str);
		Dictionary dict = new Dictionary();
		dict.readFromFile("words.txt");
		System.out.println("\nList of all the words that the string could represent:");
		printWords(str);
		System.out.println();
//		timeTaken = System.nanoTime();
		System.out.println("List of all words with the prefix of the string:");
		printPrefixWords(str);
//		timeTaken = System.nanoTime() - timeTaken;
//		System.out.println(timeTaken);
		System.out.println();
//		timeTaken = System.nanoTime();
		System.out.println("Same as above but supposedly optimised:");
		printOptPrefixWords(str);
//		timeTaken = System.nanoTime() - timeTaken;
//		System.out.println(timeTaken);
	}
}
